;========= PID TUNING — HOTEND (LEFT / RIGHT) =========
; Unified macro for PID tuning either hotend.
; User selects which tool to calibrate.

; ---- Tool Selection ----
var heaterIdx = 0
var toolIdx = 0
var headName = "Left"
var pidFile = "0:/sys/user/actions/PIDLeftHead.g"
var logPrefix = "left"

M291 S4 K{"Left Head (T0)","Right Head (T1)","Cancel"} R"PID Tuning - Select Tool" P"Select which hotend to PID tune.<br><br>Do not leave printer unattended."

if input == 0
  echo "Selected Left Head (T0) for PID tuning"
elif input == 1
  set var.heaterIdx = 1
  set var.toolIdx = 1
  set var.headName = "Right"
  set var.pidFile = "0:/sys/user/actions/PIDRightHead.g"
  set var.logPrefix = "right"
  echo "Selected Right Head (T1) for PID tuning"
else
  abort "PID tuning cancelled by user."

var otherHeaterIdx = 1 - var.heaterIdx

;========= CONFIGURABLE TEMPERATURE PARAMETERS =========
var headTargetTemp = 290     ; Default target temperature for PID tuning (°C)

; ---- Temperature Selection ----
M291 S4 K{"Start with 290°C","Custom temperature","Cancel"} R{var.headName ^ " Head PID Tuning"} P"Default target: 290°C"

if input == 0
  echo "Starting " ^ var.headName ^ " head PID tuning at default temperature: " ^ var.headTargetTemp ^ "°C"
elif input == 1
  M291 J1 L200 H450 F{var.headTargetTemp} P"Enter target temperature (200-450°C)" R"Custom PID Temperature" S5
  set var.headTargetTemp = input
  echo "Starting " ^ var.headName ^ " head PID tuning at custom temperature: " ^ var.headTargetTemp ^ "°C"
else
  abort "PID tuning cancelled by user."

; ---- Chamber Pre-Heat for High Temperature Targets ----
var useChamber = false
var chamberWaitTemp = 80                                                                      ; Chamber temp to start tuning (80°C for 300-390, 90°C for >390)

if var.headTargetTemp >= 300
    if var.headTargetTemp > 390
        set var.chamberWaitTemp = 90                                                           ; Higher targets need more chamber soak
    var chamberMsg = "Target is " ^ var.headTargetTemp ^ "°C."
    set var.chamberMsg = var.chamberMsg ^ "<br><br>Chamber heating is <b>recommended</b> for more accurate PID tuning at this temperature."
    set var.chamberMsg = var.chamberMsg ^ "<br><br>Bed will heat to 160°C and chamber to 100°C."
    set var.chamberMsg = var.chamberMsg ^ "<br>Tuning starts when chamber reaches " ^ var.chamberWaitTemp ^ "°C."
    set var.chamberMsg = var.chamberMsg ^ "<br><br>Please CLOSE the front door and lid."
    M291 S4 K{"Preheat chamber","Skip chamber heating"} F0 R"Chamber Heating Recommended" P{var.chamberMsg}
    if input == 0
        set var.useChamber = true
        echo "User selected: proceed with chamber heating"
    else
        echo "User selected: skip chamber heating"

; ---- Pre-Test Heater Fault Check ----
var faultFound = false
var faultMsg = "Heater fault(s) detected before PID tuning:<br><br>"

if heat.heaters[var.heaterIdx].state == "fault"
    set var.faultFound = true
    set var.faultMsg = var.faultMsg ^ "• <b>" ^ var.headName ^ " Hotend (H" ^ var.heaterIdx ^ ")</b> is in FAULT state<br>"

if var.useChamber && heat.heaters[3].state == "fault"
    set var.faultFound = true
    set var.faultMsg = var.faultMsg ^ "• <b>Chamber Heater (H3)</b> is in FAULT state<br>"

if var.useChamber && heat.heaters[2].state == "fault"
    set var.faultFound = true
    set var.faultMsg = var.faultMsg ^ "• <b>Bed Heater (H2)</b> is in FAULT state<br>"                ; Bed fault check — M140 S160 won't heat a faulted bed

if var.faultFound
    set var.faultMsg = var.faultMsg ^ "<br>Reset the fault and continue?"
    M98 P"0:/sys/led/fault.g"
    M291 R"Heater Fault Detected" P{var.faultMsg} S4 K{"Reset Faults & Continue","Cancel"}
    if input == 0
        M562                                                                                  ; Reset all heater faults
        G4 S2                                                                                 ; Wait for faults to clear
        ; Verify faults are cleared — also check bed (H2) since we heat it for chamber mode
        if heat.heaters[var.heaterIdx].state == "fault" || (var.useChamber && (heat.heaters[2].state == "fault" || heat.heaters[3].state == "fault"))
            M98 P"0:/sys/led/fault.g"
            M291 R"Fault Reset Failed" P"Heater faults could not be cleared.<br>Check hardware and wiring." S2
            M98 P"0:/sys/led/resetstatus.g"
            abort "Heater faults could not be cleared"
        ; Wait 5s and re-check — faults can reappear shortly after reset
        G4 S5
        if heat.heaters[var.heaterIdx].state == "fault" || (var.useChamber && (heat.heaters[2].state == "fault" || heat.heaters[3].state == "fault"))
            M98 P"0:/sys/led/fault.g"
            M291 R"Fault Reappeared" P"Heater fault reappeared after reset.<br>This indicates a hardware issue.<br>Check wiring and thermistor connections." S2
            M98 P"0:/sys/led/resetstatus.g"
            abort "Heater fault reappeared after reset"
        M98 P"0:/sys/led/resetstatus.g"
        echo "Heater faults cleared successfully"
    else
        M98 P"0:/sys/led/resetstatus.g"
        abort "PID tuning cancelled - heater faults not cleared"

; Disable Fans
M106 P3 S0
M106 P1 S0

; Cool Down Tools
M568 P0 S0 R0
M568 P1 S0 R0
M568 P2 S0 R0
M568 P3 S0 R0

; Start bed/chamber heating early so temps are maintained during cooldown and homing
if var.useChamber
    M140 S160                                                                                 ; Bed to 160°C
    M141 S100                                                                                 ; Chamber to 100°C
    echo "Bed and chamber heating started"
else
    ; No chamber selected — hold existing bed/chamber temps to prevent ambient drift during test
    if heat.heaters[2].current > 50
        M140 S{round(heat.heaters[2].current)}
        echo "Holding bed at " ^ round(heat.heaters[2].current) ^ "°C for stable ambient"
    if heat.heaters[3].current > 40
        M141 S{round(heat.heaters[3].current)}
        echo "Holding chamber at " ^ round(heat.heaters[3].current) ^ "°C for stable ambient"

; ---- Read configuration version ----
var macrosVersionContent = fileread("0:/sys/version.txt", 0, 2, ',')
var macrosVersion = var.macrosVersionContent[0] ^ ""
var macrosReleaseDate = var.macrosVersionContent[1] ^ ""

; ---- Create Temperature Log File (fixed name — each run overwrites previous) ----
; Start logging immediately so we capture cooldown, homing, and chamber pre-heat data
var logFile = "0:/sys/logs/pid_" ^ var.logPrefix ^ ".csv"
var paramFile = "0:/sys/logs/pid_" ^ var.logPrefix ^ ".txt"
var logLine = ""

; Write test settings
echo >{var.logFile} "# PID Tuning — " ^ var.headName ^ " Hotend (H" ^ var.heaterIdx ^ ")"
echo >>{var.logFile} "# Target: " ^ var.headTargetTemp ^ "C"
echo >>{var.logFile} "# Chamber: " ^ (var.useChamber ? "Yes (wait " ^ var.chamberWaitTemp ^ "C)" : "No")
echo >>{var.logFile} "# Old Model: R" ^ heat.heaters[var.heaterIdx].model.heatingRate ^ " K" ^ heat.heaters[var.heaterIdx].model.coolingRate ^ " D" ^ heat.heaters[var.heaterIdx].model.deadTime ^ " E" ^ heat.heaters[var.heaterIdx].model.coolingExp
echo >>{var.logFile} "# Mainboard FW: " ^ boards[0].firmwareFileName ^ " v" ^ boards[0].firmwareVersion
echo >>{var.logFile} "# Expansion FW: " ^ boards[1].firmwareFileName ^ " v" ^ boards[1].firmwareVersion
echo >>{var.logFile} "# Config: v" ^ var.macrosVersion ^ " (" ^ var.macrosReleaseDate ^ ")"
echo >>{var.logFile} "#"

var logHeader = "Elapsed(s)," ^ var.headName ^ "Nozzle(C),OtherNozzle(C),Bed(C),ChamberAir(C),"
set var.logHeader = var.logHeader ^ var.headName ^ "State,HEPA(RPM)"
echo >>{var.logFile} var.logHeader

var startTime = state.upTime
var elapsed = 0

; Log initial state
set var.logLine = "0," ^ heat.heaters[var.heaterIdx].current
set var.logLine = var.logLine ^ "," ^ heat.heaters[var.otherHeaterIdx].current
set var.logLine = var.logLine ^ "," ^ heat.heaters[2].current
set var.logLine = var.logLine ^ "," ^ heat.heaters[3].current
set var.logLine = var.logLine ^ "," ^ heat.heaters[var.heaterIdx].state
set var.logLine = var.logLine ^ "," ^ fans[7].rpm
echo >>{var.logFile} var.logLine
echo "=== PID Tuning Log Started: " ^ var.logFile ^ " ==="

; ---- Save Heater Configuration Parameters ----
var paramLine = ""
echo >{var.paramFile} "=== PID Tuning Configuration Snapshot ==="
echo >>{var.paramFile} "Date: " ^ state.time
echo >>{var.paramFile} "Heater: H" ^ var.heaterIdx ^ " (" ^ var.headName ^ " Hotend)"
echo >>{var.paramFile} "Target: " ^ var.headTargetTemp ^ "C"
echo >>{var.paramFile} "Chamber: " ^ (var.useChamber ? "Yes (wait " ^ var.chamberWaitTemp ^ "C)" : "No")
echo >>{var.paramFile} ""

; H0 - Left Nozzle
echo >>{var.paramFile} "--- H0 (Left Nozzle) ---"
set var.paramLine = "M307 H0 R" ^ heat.heaters[0].model.heatingRate
set var.paramLine = var.paramLine ^ " K" ^ heat.heaters[0].model.coolingRate ^ ":" ^ heat.heaters[0].model.fanCoolingRate
set var.paramLine = var.paramLine ^ " D" ^ heat.heaters[0].model.deadTime
set var.paramLine = var.paramLine ^ " E" ^ heat.heaters[0].model.coolingExp
set var.paramLine = var.paramLine ^ " S" ^ heat.heaters[0].model.enabled
echo >>{var.paramFile} var.paramLine
if #heat.heaters[0].monitors > 0
  var h0m = 0
  while var.h0m < #heat.heaters[0].monitors
    if heat.heaters[0].monitors[var.h0m].condition != "disabled"
      echo >>{var.paramFile} "  Monitor " ^ var.h0m ^ ": condition=" ^ heat.heaters[0].monitors[var.h0m].condition ^ " limit=" ^ heat.heaters[0].monitors[var.h0m].limit ^ " action=" ^ heat.heaters[0].monitors[var.h0m].action
    set var.h0m = var.h0m + 1
echo >>{var.paramFile} ""

; H1 - Right Nozzle
echo >>{var.paramFile} "--- H1 (Right Nozzle) ---"
set var.paramLine = "M307 H1 R" ^ heat.heaters[1].model.heatingRate
set var.paramLine = var.paramLine ^ " K" ^ heat.heaters[1].model.coolingRate ^ ":" ^ heat.heaters[1].model.fanCoolingRate
set var.paramLine = var.paramLine ^ " D" ^ heat.heaters[1].model.deadTime
set var.paramLine = var.paramLine ^ " E" ^ heat.heaters[1].model.coolingExp
set var.paramLine = var.paramLine ^ " S" ^ heat.heaters[1].model.enabled
echo >>{var.paramFile} var.paramLine
if #heat.heaters[1].monitors > 0
  var h1m = 0
  while var.h1m < #heat.heaters[1].monitors
    if heat.heaters[1].monitors[var.h1m].condition != "disabled"
      echo >>{var.paramFile} "  Monitor " ^ var.h1m ^ ": condition=" ^ heat.heaters[1].monitors[var.h1m].condition ^ " limit=" ^ heat.heaters[1].monitors[var.h1m].limit ^ " action=" ^ heat.heaters[1].monitors[var.h1m].action
    set var.h1m = var.h1m + 1
echo >>{var.paramFile} ""

; H2 - Bed
echo >>{var.paramFile} "--- H2 (Bed Heater) ---"
set var.paramLine = "M307 H2 R" ^ heat.heaters[2].model.heatingRate
set var.paramLine = var.paramLine ^ " K" ^ heat.heaters[2].model.coolingRate ^ ":" ^ heat.heaters[2].model.fanCoolingRate
set var.paramLine = var.paramLine ^ " D" ^ heat.heaters[2].model.deadTime
set var.paramLine = var.paramLine ^ " E" ^ heat.heaters[2].model.coolingExp
set var.paramLine = var.paramLine ^ " S" ^ heat.heaters[2].model.enabled
echo >>{var.paramFile} var.paramLine
if #heat.heaters[2].monitors > 0
  var h2m = 0
  while var.h2m < #heat.heaters[2].monitors
    if heat.heaters[2].monitors[var.h2m].condition != "disabled"
      echo >>{var.paramFile} "  Monitor " ^ var.h2m ^ ": condition=" ^ heat.heaters[2].monitors[var.h2m].condition ^ " limit=" ^ heat.heaters[2].monitors[var.h2m].limit ^ " action=" ^ heat.heaters[2].monitors[var.h2m].action
    set var.h2m = var.h2m + 1
echo >>{var.paramFile} ""

; H3 - Chamber
echo >>{var.paramFile} "--- H3 (Chamber Heater) ---"
set var.paramLine = "M307 H3 R" ^ heat.heaters[3].model.heatingRate
set var.paramLine = var.paramLine ^ " K" ^ heat.heaters[3].model.coolingRate ^ ":" ^ heat.heaters[3].model.fanCoolingRate
set var.paramLine = var.paramLine ^ " D" ^ heat.heaters[3].model.deadTime
set var.paramLine = var.paramLine ^ " E" ^ heat.heaters[3].model.coolingExp
set var.paramLine = var.paramLine ^ " S" ^ heat.heaters[3].model.enabled
echo >>{var.paramFile} var.paramLine
if #heat.heaters[3].monitors > 0
  var h3m = 0
  while var.h3m < #heat.heaters[3].monitors
    if heat.heaters[3].monitors[var.h3m].condition != "disabled"
      echo >>{var.paramFile} "  Monitor " ^ var.h3m ^ ": condition=" ^ heat.heaters[3].monitors[var.h3m].condition ^ " limit=" ^ heat.heaters[3].monitors[var.h3m].limit ^ " action=" ^ heat.heaters[3].monitors[var.h3m].action
    set var.h3m = var.h3m + 1

; Heater PWM and Heating Rates
echo >>{var.paramFile} ""
echo >>{var.paramFile} "=== Heater PWM (at test start) ==="
echo >>{var.paramFile} "H0 (Left Nozzle)  avgPwm: " ^ heat.heaters[0].avgPwm
echo >>{var.paramFile} "H1 (Right Nozzle) avgPwm: " ^ heat.heaters[1].avgPwm
echo >>{var.paramFile} "H2 (Bed Heater)   avgPwm: " ^ heat.heaters[2].avgPwm
echo >>{var.paramFile} "H3 (Chamber)      avgPwm: " ^ heat.heaters[3].avgPwm

echo >>{var.paramFile} ""
echo >>{var.paramFile} "=== Heating Rates (from model) ==="
echo >>{var.paramFile} "H0 heatingRate: " ^ heat.heaters[0].model.heatingRate
echo >>{var.paramFile} "H1 heatingRate: " ^ heat.heaters[1].model.heatingRate
echo >>{var.paramFile} "H2 heatingRate: " ^ heat.heaters[2].model.heatingRate
echo >>{var.paramFile} "H3 heatingRate: " ^ heat.heaters[3].model.heatingRate

echo >>{var.paramFile} ""
echo >>{var.paramFile} "=== Board Voltages (at test start) ==="
echo >>{var.paramFile} "Board 0 Vin: " ^ boards[0].vIn.current ^ "V (min: " ^ boards[0].vIn.min ^ " max: " ^ boards[0].vIn.max ^ ")"
echo >>{var.paramFile} "Board 1 Vin: " ^ boards[1].vIn.current ^ "V (min: " ^ boards[1].vIn.min ^ " max: " ^ boards[1].vIn.max ^ ")"

echo >>{var.paramFile} ""
echo >>{var.paramFile} "=== MCU Temperatures (at test start) ==="
echo >>{var.paramFile} "Board 0 MCU: " ^ boards[0].mcuTemp.current ^ "C (min: " ^ boards[0].mcuTemp.min ^ " max: " ^ boards[0].mcuTemp.max ^ ")"
echo >>{var.paramFile} "Board 1 MCU: " ^ boards[1].mcuTemp.current ^ "C (min: " ^ boards[1].mcuTemp.min ^ " max: " ^ boards[1].mcuTemp.max ^ ")"

echo >>{var.paramFile} ""
echo >>{var.paramFile} "=== Firmware ==="
echo >>{var.paramFile} "Board 0: " ^ boards[0].firmwareFileName ^ " v" ^ boards[0].firmwareVersion
echo >>{var.paramFile} "Board 1: " ^ boards[1].firmwareFileName ^ " v" ^ boards[1].firmwareVersion
echo >>{var.paramFile} "Config:  v" ^ var.macrosVersion ^ " (" ^ var.macrosReleaseDate ^ ")"

echo "Heater parameters saved to: " ^ var.paramFile

; ---- Save old model parameters to detect if tuning actually updated them ----
var oldHeatingRate = heat.heaters[var.heaterIdx].model.heatingRate
var oldCoolingRate = heat.heaters[var.heaterIdx].model.coolingRate
var oldDeadTime = heat.heaters[var.heaterIdx].model.deadTime
var oldCoolingExp = heat.heaters[var.heaterIdx].model.coolingExp

; ---- Cooldown Wait with Skip Option ----
var coolThreshold = 100
if var.useChamber
    set var.coolThreshold = 200
if heat.heaters[var.heaterIdx].current > var.coolThreshold
    var coolMsg = var.headName ^ " hotend is at " ^ heat.heaters[var.heaterIdx].current ^ "°C."
    set var.coolMsg = var.coolMsg ^ " It needs to cool below " ^ var.coolThreshold ^ "°C for accurate tuning."
    set var.coolMsg = var.coolMsg ^ "<br><br>Wait for cool down, or skip to start now?"
    M291 R{var.headName ^ " Head Cool Down"} P{var.coolMsg} S4 K{"Wait for cool down","Skip - start now"}
    if input == 0
        M291 R"Cooling Down..." P{"Waiting for " ^ var.headName ^ " hotend to cool below " ^ var.coolThreshold ^ "°C."} S1 T10
        while heat.heaters[var.heaterIdx].current > var.coolThreshold
            G4 S1
            set var.elapsed = state.upTime - var.startTime
            set var.logLine = var.elapsed ^ "," ^ heat.heaters[var.heaterIdx].current
            set var.logLine = var.logLine ^ "," ^ heat.heaters[var.otherHeaterIdx].current
            set var.logLine = var.logLine ^ "," ^ heat.heaters[2].current
            set var.logLine = var.logLine ^ "," ^ heat.heaters[3].current
            set var.logLine = var.logLine ^ "," ^ heat.heaters[var.heaterIdx].state
            set var.logLine = var.logLine ^ "," ^ fans[7].rpm
            echo >>{var.logFile} var.logLine
        echo var.headName ^ " hotend cooled to " ^ heat.heaters[var.heaterIdx].current ^ "°C"
    else
        echo "User skipped cool down (" ^ var.headName ^ " at " ^ heat.heaters[var.heaterIdx].current ^ "°C)"

; ---- Home all axes if not already homed ----
if !move.axes[0].homed || !move.axes[1].homed || !move.axes[2].homed || !move.axes[3].homed
    M291 R"Homing" P"Homing all axes before positioning..." S1 T5
    M98 P"homeall.g" S1 N1                                                       ; N1 = skip LED reset

; ---- Position and Wait for Chamber Pre-Heat ----
if var.useChamber
    G1 X-100 U100 Y0 F6000
    G1 Z350 F600
    M400
    M291 R"Chamber Pre-Heat" P{"Heating bed to 160°C and chamber to 100°C.<br>Waiting for chamber to reach " ^ var.chamberWaitTemp ^ "°C...<br><br>Ensure front door and lid are closed."} S1 T10
    while heat.heaters[3].current < var.chamberWaitTemp
        G4 S1
        set var.elapsed = state.upTime - var.startTime
        set var.logLine = var.elapsed ^ "," ^ heat.heaters[var.heaterIdx].current
        set var.logLine = var.logLine ^ "," ^ heat.heaters[var.otherHeaterIdx].current
        set var.logLine = var.logLine ^ "," ^ heat.heaters[2].current
        set var.logLine = var.logLine ^ "," ^ heat.heaters[3].current
        set var.logLine = var.logLine ^ "," ^ heat.heaters[var.heaterIdx].state
        set var.logLine = var.logLine ^ "," ^ fans[7].rpm
        echo >>{var.logFile} var.logLine
    echo "Chamber reached " ^ heat.heaters[3].current ^ "°C - proceeding with PID tuning"

; ---- LED: Blue for heating phase ----
M98 P"0:/sys/led/dimmwhite.g"
M98 P"0:/sys/led/start_cold.g"

; Validate chamber air temperature before tuning
var ambient = heat.heaters[3].current
echo >>{var.logFile} "# Ambient at tuning start: " ^ var.ambient ^ "C"
if var.useChamber
    if var.ambient < 10
        M98 P"0:/sys/led/fault.g"
        M291 R"Chamber Temperature Too Low" P{"Chamber air temperature "^var.ambient^"°C is below 10°C."} S2
        M98 P"0:/sys/led/resetstatus.g"
        abort "Error: Chamber air temperature is not within range to start."

; ---- LED: Yellow for tuning phase ----
M98 P"0:/sys/led/statusoff.g"
M98 P"0:/sys/led/pause.g"

; ---- Step 1: Start M303 and check if the command was accepted ----
M303 T{var.toolIdx} S{var.headTargetTemp} A{var.ambient} F1 Q1
if result != 0
    M98 P"0:/sys/led/fault.g"
    echo >>{var.logFile} "RESULT: M303 command rejected (result=" ^ result ^ ")"
    var rejMsg = "PID tuning could not start.<br><br>"
    set var.rejMsg = var.rejMsg ^ "The M303 command was rejected by the firmware.<br>"
    set var.rejMsg = var.rejMsg ^ "Check that " ^ var.headName ^ " hotend (H" ^ var.heaterIdx ^ ") is properly configured."
    M291 R"PID Tune Failed" P{var.rejMsg} S2
    M98 P"0:/sys/led/resetstatus.g"
    M141 S0
    M140 S0
    abort "Error: M303 command was rejected — PID tuning could not start."

; ---- Step 2: Verify that tuning actually entered the "tuning" state ----
G4 S2                                                                                     ; Brief pause to let the heater state update
var tuningStarted = (heat.heaters[var.heaterIdx].state == "tuning")

if !var.tuningStarted
    M98 P"0:/sys/led/fault.g"
    echo >>{var.logFile} "RESULT: Heater never entered tuning state (state=" ^ heat.heaters[var.heaterIdx].state ^ ")"
    var stateMsg = "PID tuning was accepted but the heater never entered the <b>tuning</b> state.<br><br>"
    set var.stateMsg = var.stateMsg ^ "Current heater state: <b>" ^ heat.heaters[var.heaterIdx].state ^ "</b><br><br>"
    set var.stateMsg = var.stateMsg ^ "This may indicate a hardware issue or heater fault."
    M291 R"PID Tune Failed" P{var.stateMsg} S2
    if heat.heaters[var.heaterIdx].state == "fault"
        M562                                                                              ; Reset heater faults
    M98 P"0:/sys/led/resetstatus.g"
    M141 S0
    M140 S0
    abort "Error: Heater never entered tuning state."

; ---- Step 3: Monitor tuning progress ----
while heat.heaters[var.heaterIdx].state == "tuning"
    G4 S1
    set var.elapsed = state.upTime - var.startTime
    set var.logLine = var.elapsed ^ "," ^ heat.heaters[var.heaterIdx].current
    set var.logLine = var.logLine ^ "," ^ heat.heaters[var.otherHeaterIdx].current
    set var.logLine = var.logLine ^ "," ^ heat.heaters[2].current
    set var.logLine = var.logLine ^ "," ^ heat.heaters[3].current
    set var.logLine = var.logLine ^ "," ^ heat.heaters[var.heaterIdx].state
    set var.logLine = var.logLine ^ "," ^ fans[7].rpm
    echo >>{var.logFile} var.logLine

; ---- Step 4: Post-Tuning Validation ----
; When tuning ends, heater goes to "fault" on failure or "off" on success.

; 4a. Check for heater fault — this is the definitive failure indicator
if heat.heaters[var.heaterIdx].state == "fault"
    M98 P"0:/sys/led/fault.g"
    ; Determine why it failed: did the temperature reach the target?
    if heat.heaters[var.heaterIdx].current < (var.headTargetTemp - 20)
        ; Temperature was NOT reached
        echo >>{var.logFile} "RESULT: PID tuning FAILED - target temperature was not reached"
        var tempMsg = "Auto tune cancelled — <b>target temperature was not reached</b>.<br><br>"
        set var.tempMsg = var.tempMsg ^ "Target: " ^ var.headTargetTemp ^ "°C | Current: " ^ heat.heaters[var.heaterIdx].current ^ "°C<br><br>"
        set var.tempMsg = var.tempMsg ^ "<b>Possible causes:</b><br>"
        set var.tempMsg = var.tempMsg ^ "• Faulty heater cartridge or wiring<br>"
        set var.tempMsg = var.tempMsg ^ "• Hotend not properly assembled<br>"
        set var.tempMsg = var.tempMsg ^ "• Thermistor not seated correctly<br>"
        set var.tempMsg = var.tempMsg ^ "• Ambient temperature too low"
        M291 R"PID Tune Failed" P{var.tempMsg} S2
    else
        ; Temperature WAS reached, but tuning still failed
        echo >>{var.logFile} "RESULT: PID tuning FAILED - heater fault (temperature was reached)"
        var hwMsg = "PID tuning <b>failed</b> — the " ^ var.headName ^ " heater entered a fault state.<br><br>"
        set var.hwMsg = var.hwMsg ^ "The target (" ^ var.headTargetTemp ^ "°C) was reached, but tuning could not complete.<br><br>"
        set var.hwMsg = var.hwMsg ^ "<b>Possible causes:</b><br>"
        set var.hwMsg = var.hwMsg ^ "• Temperature oscillation too large<br>"
        set var.hwMsg = var.hwMsg ^ "• Unstable thermistor connection<br>"
        set var.hwMsg = var.hwMsg ^ "• Electrical interference or loose wiring"
        M291 R"PID Tune Failed" P{var.hwMsg} S2
    M562                                                                                  ; Reset heater faults
    M98 P"0:/sys/led/resetstatus.g"
    M141 S0
    M140 S0
    abort "PID tuning failed - heater fault after tuning"

; 4b. Heater is not in fault — verify model parameters actually changed (extra safety)
if heat.heaters[var.heaterIdx].model.heatingRate == var.oldHeatingRate && heat.heaters[var.heaterIdx].model.deadTime == var.oldDeadTime
    ; Model params didn't change — tuning may not have completed properly
    M98 P"0:/sys/led/fault.g"
    echo >>{var.logFile} "RESULT: PID tuning FAILED - model parameters unchanged"
    var noChangeMsg = "PID tuning completed without errors, but the heater model parameters <b>did not change</b>.<br><br>"
    set var.noChangeMsg = var.noChangeMsg ^ "This may indicate that tuning did not produce valid results.<br><br>"
    set var.noChangeMsg = var.noChangeMsg ^ "Consider retrying with different settings."
    M291 R"PID Tune Warning" P{var.noChangeMsg} S2
    M98 P"0:/sys/led/resetstatus.g"
    M141 S0
    M140 S0
    abort "PID tuning failed - model parameters did not change"

; ---- Tuning succeeded — save results first, then notify user ----

; Log final entry
var totalTime = state.upTime - var.startTime
set var.logLine = var.totalTime ^ "," ^ heat.heaters[var.heaterIdx].current
set var.logLine = var.logLine ^ "," ^ heat.heaters[var.otherHeaterIdx].current
set var.logLine = var.logLine ^ "," ^ heat.heaters[2].current
set var.logLine = var.logLine ^ "," ^ heat.heaters[3].current
set var.logLine = var.logLine ^ "," ^ heat.heaters[var.heaterIdx].state
set var.logLine = var.logLine ^ "," ^ fans[7].rpm
echo >>{var.logFile} var.logLine
echo >>{var.logFile} "RESULT: PID tuning completed in " ^ var.totalTime ^ "s"
echo "=== PID Tuning Log Complete: " ^ var.totalTime ^ "s ==="

; Read new model parameters
var rr = heat.heaters[var.heaterIdx].model.heatingRate
var kk1 = heat.heaters[var.heaterIdx].model.coolingRate
var kk2 = heat.heaters[var.heaterIdx].model.fanCoolingRate
var dd = heat.heaters[var.heaterIdx].model.deadTime
var ee = heat.heaters[var.heaterIdx].model.coolingExp
var vv = heat.heaters[var.heaterIdx].model.standardVoltage

; Save PID values to file
echo >{var.pidFile} "M307 H"^var.heaterIdx^" R"^{var.rr}^" K"^{var.kk1}^":"^{var.kk2}^" D"^{var.dd}^" E"^{var.ee}^" S1.00 B0 V"^{var.vv}

; ---- Turn off chamber and bed heaters ----
M141 S0
M140 S0
echo "Bed and chamber heaters turned off after PID tuning"

; ---- LED: Green for success ----
M98 P"0:/sys/led/end.g"

; Build old vs new comparison message
var okMsg = "<b>" ^ var.headName ^ "-head PID tuning complete!</b><br><br>"
set var.okMsg = var.okMsg ^ "<b>Old values:</b><br>"
set var.okMsg = var.okMsg ^ "R" ^ var.oldHeatingRate ^ " K" ^ var.oldCoolingRate
set var.okMsg = var.okMsg ^ " D" ^ var.oldDeadTime ^ " E" ^ var.oldCoolingExp ^ "<br><br>"
set var.okMsg = var.okMsg ^ "<b>New values:</b><br>"
set var.okMsg = var.okMsg ^ "R" ^ var.rr ^ " K" ^ var.kk1
set var.okMsg = var.okMsg ^ " D" ^ var.dd ^ " E" ^ var.ee
M291 R"PID Tune Complete" P{var.okMsg} S2

; ---- LED: Return to white after user acknowledges ----
M98 P"0:/sys/led/resetstatus.g"